<template>
    <div class="gltf-zip-loader">
        <div ref="dropZoneRef" class="drop-zone" :class="{ 'drag-over': isDragOver, 'is-loading': isLoading }"
            @dragover.prevent="handleDragOver" @dragleave.prevent="handleDragLeave" @drop.prevent="handleDrop"
            @click="triggerFileInput" role="button" tabindex="0" aria-label="Drop ZIP file here or click to select">
            <!-- Hidden File Input -->
            <input ref="fileInputRef" type="file" accept=".zip" style="display: none" @change="handleFileSelect"
                :disabled="isLoading" />

            <!-- Content Area -->
            <div v-if="isLoading" class="loading-overlay">
                <div class="spinner"></div>
                <span>Loading Model...</span>
            </div>
            <div v-else-if="errorMessage" class="status-message error-message">
                <p>Error:</p>
                <p>{{ errorMessage }}</p>
                <p>Click or drop a new file.</p>
            </div>
            <div v-else-if="loadedModelName" class="status-message success-message">
                <p>Loaded: {{ loadedModelName }}</p>
                <p>Click or drop a new file to replace.</p>
            </div>
            <div v-else class="placeholder-text">
                <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-upload"
                    viewBox="0 0 16 16">
                    <path
                        d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" />
                    <path
                        d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z" />
                </svg>
                <span>Drop ZIP file here</span>
                <span>or <strong>click to select</strong></span>
            </div>
        </div>

        <!-- Optional Preview Area -->
        <!-- <div v-if="props.showPreview" class="preview-area">
            <canvas ref="previewCanvasRef" :style="{ width: props.previewWidth, height: props.previewHeight }"></canvas>
            <p v-if="!isLoading && !loadedModelName && !errorMessage" class="preview-placeholder">Preview will appear
                here</p>
        </div> -->

    </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import JSZip from 'jszip';

// --- Props ---
const props = defineProps({
    gltfPathInZip: {
        type: String,
        required: true, // Crucial: specify the exact path like 'folder/model.gltf'
        validator: (value) => value.endsWith('.gltf') || value.endsWith('.glb'),
    },
    showPreview: {
        type: Boolean,
        default: false,
    },
    previewWidth: {
        type: [String, Number],
        default: '100%'
    },
    previewHeight: {
        type: [String, Number],
        default: '300px'
    }
});

// --- Emits ---
const emit = defineEmits(['scene', 'loading', 'error']);

// --- Refs ---
const dropZoneRef = ref(null);
const fileInputRef = ref(null);
const previewCanvasRef = ref(null); // Ref for the preview canvas

// --- State ---
const isDragOver = ref(false);
const isLoading = ref(false);
const errorMessage = ref(null);
const loadedModelName = ref(null);
const objectURLs = ref([]); // Store blob URLs for cleanup

// --- Preview State (only if showPreview is true) ---
let renderer = null;
let previewScene = null;
let camera = null;
let controls = null;
let previewModel = null;
let animationFrameId = null;

// --- Event Handlers ---
const handleDragOver = (event) => {
    if (!isLoading.value) {
        isDragOver.value = true;
    }
};

const handleDragLeave = (event) => {
    isDragOver.value = false;
};

const handleDrop = (event) => {
    if (isLoading.value) return;
    isDragOver.value = false;
    const files = event.dataTransfer.files;
    if (files.length > 0) {
        handleFile(files[0]);
    }
};

const handleFileSelect = (event) => {
    if (isLoading.value) return;
    const files = event.target.files;
    if (files.length > 0) {
        handleFile(files[0]);
    }
    // Reset input value to allow selecting the same file again
    if (fileInputRef.value) {
        fileInputRef.value.value = null;
    }
};

const triggerFileInput = () => {
    if (isLoading.value) return;
    fileInputRef.value?.click();
};

// --- Core Logic ---
const handleFile = (file) => {
    if (!file) return;

    if (!file.name.toLowerCase().endsWith('.zip')) {
        errorMessage.value = 'Invalid file type. Please provide a .zip file.';
        emit('error', errorMessage.value);
        loadedModelName.value = null;
        cleanupPreview(); // Clear preview on error
        return;
    }

    // --- Start Loading Process ---
    isLoading.value = true;
    errorMessage.value = null;
    loadedModelName.value = null;
    emit('loading', true);
    cleanupObjectUrls(); // Clean up URLs from previous load
    if (props.showPreview) cleanupPreview(); // Clear previous preview


    loadModelFromZip(file, props.gltfPathInZip)
        .then(gltf => {
            console.log('Model loaded successfully in component');
            loadedModelName.value = file.name; // Show filename on success
            emit('scene', gltf.scene); // Emit the loaded scene!

            if (props.showPreview && gltf.scene) {
                nextTick(() => { // Ensure canvas is ready
                    setupPreview(gltf.scene);
                });
            }
        })
        .catch(error => {
            console.error("Error loading model in component:", error);
            errorMessage.value = error.message || 'Failed to load model from zip.';
            emit('error', error);
            cleanupPreview(); // Clear preview on error
        })
        .finally(() => {
            isLoading.value = false;
            emit('loading', false);
            // Don't cleanup objectURLs here, they might be needed by textures
        });
};

async function loadModelFromZip(zipFileObject, gltfPathInZip) {
    const loadingManager = new THREE.LoadingManager();
    const gltfLoader = new GLTFLoader(loadingManager);
    const currentObjectURLs = []; // Local array for this load operation to manage URLs

    try {
        console.log("Loading ZIP file data...");
        const zip = await JSZip.loadAsync(zipFileObject);
        console.log("ZIP file loaded, extracting contents. Checking paths...");
        console.log(`Searching for prop path: '${gltfPathInZip}'`); // Log the target path

        const filePromises = [];
        let foundGltf = false;
        let basePath = gltfPathInZip.substring(0, gltfPathInZip.lastIndexOf('/') + 1); // Directory of the GLTF file

        // *** START DEBUG LOGGING ***
        const foundPaths = []; // Array to store all found file/dir paths for logging
        zip.forEach((relativePath, zipEntry) => {
            // Normalize path separators for consistent comparison
            const normalizedPath = relativePath.replace(/\\/g, '/');
            foundPaths.push(normalizedPath); // Collect all paths found

            // Process only files, not directories explicitly marked as such
            if (!zipEntry.dir) {
                // console.log(`Found file in zip: '${normalizedPath}' (Original: '${relativePath}')`); // Log every file path (can be very verbose)

                // Check if this file is the target GLTF file
                if (normalizedPath === gltfPathInZip) {
                    console.log(`>>> Exact match found for GLTF: ${normalizedPath}`); // Log the successful match
                    foundGltf = true;
                }

                // Create a promise to extract the file content as a Blob and create a URL
                const promise = zipEntry.async('blob').then(blob => {
                    const objectURL = URL.createObjectURL(blob);
                    // Track the path and URL pair created during this specific load attempt
                    currentObjectURLs.push({ path: normalizedPath, url: objectURL });
                    // Return structured data for Promise.all
                    return { path: normalizedPath, url: objectURL, blob: blob };
                }).catch(err => {
                    console.error(`Error processing zip entry ${normalizedPath}:`, err);
                    return null; // Handle potential errors during extraction
                });
                filePromises.push(promise);

            } else {
                // Optionally log directories if helpful for debugging structure
                // console.log(`Found directory in zip: '${normalizedPath}'`);
            }
        });
        // *** END DEBUG LOGGING ***

        // Log all found paths *after* iterating, potentially easier to scan in console
        console.log("All paths (files & dirs) found in ZIP:", foundPaths);
        console.log("Non-directory paths found:", foundPaths.filter(p => !p.endsWith('/')));


        // Check if the target GLTF file path was actually found during iteration
        if (!foundGltf) {
            console.error("!!! GLTF PATH CHECK FAILED !!!"); // Make failure obvious in logs
            console.error(`Searched for: '${gltfPathInZip}'`);
            console.error("Did not find an exact match among the non-directory paths listed above.");
            // Throw the specific error indicating the file wasn't found
            throw new Error(`Specified GLTF file ('${gltfPathInZip}') not found inside the zip.`);
        }

        // Wait for all file extraction promises to complete
        const extractedFilesRaw = await Promise.all(filePromises);
        // Filter out any null results from failed extractions
        const extractedFiles = extractedFilesRaw.filter(file => file !== null);

        // Create a Map for efficient lookup of file path to its Blob URL
        const fileMap = new Map();
        extractedFiles.forEach(file => fileMap.set(file.path, file.url));
        console.log("Blob URLs created for extracted files.");

        // Store the successfully created Blob URLs in the component's reactive ref
        // This allows them to be cleaned up later (e.g., on unmount or next load)
        objectURLs.value = currentObjectURLs.map(item => item.url);

        // Configure the Three.js Loading Manager to resolve paths using our Blob URLs
        loadingManager.setURLModifier((url) => {
            console.log(`[URL Modifier V5] Intercepted request for: '${url}'`);
            const validBlobUrls = Array.from(fileMap.values());

            // 1. Check if it's one of OUR valid Blob URLs (usually just the main GLTF)
            if (validBlobUrls.includes(url)) {
                console.log(`[URL Modifier V5] URL is a VALID known Blob URL. Passing through: '${url}'`);
                return url;
            }

            // 2. If not a known Blob URL, extract the intended path key for fileMap lookup
            let intendedMapKey = '';
            if (url.startsWith('blob:')) {
                // --- Robust String Manipulation ---
                // Assume structure is blob:http://<origin>/<path>
                // Find the first '/' *after* the '://' part to reliably get the start of the path
                let pathStartIndex = -1;
                const originEndMarker = '://';
                const originEndIndex = url.indexOf(originEndMarker);

                if (originEndIndex !== -1) {
                    // Start searching for the path separator '/' after the origin marker
                    pathStartIndex = url.indexOf('/', originEndIndex + originEndMarker.length);
                } else {
                    // Fallback if '://' isn't found (unlikely for http blob URLs)
                    // Try finding the first slash after the scheme part (e.g., after 'blob:')
                    pathStartIndex = url.indexOf('/', url.indexOf(':') + 1);
                }


                if (pathStartIndex !== -1) {
                    // Extract the path part (e.g., '/scene.bin' or '/textures/BodyGlass_baseColor.png')
                    let pathPart = url.substring(pathStartIndex);
                    // Remove the leading '/' to match the keys in fileMap
                    intendedMapKey = pathPart.startsWith('/') ? pathPart.substring(1) : pathPart;
                    console.log(`[URL Modifier V5] Extracted map key '${intendedMapKey}' from blob URL using string manipulation.`);
                } else {
                    // If we couldn't find the path start, something is very wrong with the URL format
                    console.warn(`[URL Modifier V5] Could not reliably extract path part from blob URL: ${url}. Falling back to using the original URL as key.`);
                    intendedMapKey = url; // Fallback, likely won't work but prevents crashing
                }
                // --- End Robust String Manipulation ---
            } else {
                // Assume it's already the correct relative path (used as key)
                intendedMapKey = url.replace(/\\/g, '/'); // Normalize just in case
                console.log(`[URL Modifier V5] Assuming relative path is map key: '${intendedMapKey}'`);
            }

            // 3. Attempt direct lookup in fileMap using the determined key
            console.log(`[URL Modifier V5] Attempting direct lookup with key: '${intendedMapKey}'`);
            const resolvedBlobUrl = fileMap.get(intendedMapKey);

            // 4. Return result or log failure
            if (resolvedBlobUrl) {
                console.log(`[URL Modifier V5] SUCCESS: Redirecting request representing '${url}' to Blob URL: '${resolvedBlobUrl}' (Found via key '${intendedMapKey}')`);
                return resolvedBlobUrl; // Return the CORRECT Blob URL
            } else {
                console.warn(`[URL Modifier V5] FAILURE: Could not find Blob URL for request '${url}'. Tried direct key '${intendedMapKey}'.`);
                console.warn('[URL Modifier V5] Available keys in fileMap:', Array.from(fileMap.keys()));
                return url; // Fallback - the loader will likely fail for this resource
            }
        });


        // Get the Blob URL for the main GLTF file we confirmed exists
        const mainGltfBlobUrl = fileMap.get(gltfPathInZip);
        if (!mainGltfBlobUrl) {
            // This should theoretically not happen due to the 'foundGltf' check earlier, but adds robustness
            throw new Error(`Consistency error: Could not retrieve Blob URL for main GLTF file: ${gltfPathInZip} even though it was found.`);
        }

        console.log(`Starting GLTF load with main Blob URL for path: ${gltfPathInZip}`);
        // Load the GLTF model using the main Blob URL and the configured manager
        const gltf = await gltfLoader.loadAsync(mainGltfBlobUrl);
        console.log("GLTF loaded successfully by loader.");

        // Return the loaded GLTF object (contains gltf.scene, gltf.animations etc.)
        return gltf;

    } catch (error) {
        // Log the error that occurred during the process
        console.error("Error during ZIP processing or GLTF loading:", error);

        // Important: Clean up any Blob URLs that were created *during this specific attempt*
        // even if it failed, to prevent memory leaks.
        console.log(`Cleaning up ${currentObjectURLs.length} Blob URLs from failed load attempt.`);
        currentObjectURLs.forEach(item => URL.revokeObjectURL(item.url));

        // Re-throw the error so the calling function (handleFile) can catch it
        // and update the component's state (e.g., show error message)
        throw error;
    }
}

// --- Preview Logic ---

function setupPreview(modelScene) {
    if (!props.showPreview || !previewCanvasRef.value) return;
    cleanupPreview(); // Clean up any previous instance

    try {
        // Basic scene
        previewScene = new THREE.Scene();
        previewScene.background = new THREE.Color(0xeeeeee);

        // Camera
        const canvas = previewCanvasRef.value;
        const aspect = canvas.clientWidth / canvas.clientHeight;
        camera = new THREE.PerspectiveCamera(50, aspect, 0.1, 100);
        camera.position.set(0, 1.5, 5); // Adjust as needed

        // Renderer
        renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true });
        renderer.setSize(canvas.clientWidth, canvas.clientHeight, false); // Use clientWidth/Height for sizing
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.outputColorSpace = THREE.SRGBColorSpace;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 1.0;

        // Lights
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
        previewScene.add(ambientLight);
        const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
        dirLight.position.set(5, 10, 7);
        previewScene.add(dirLight);

        // Controls
        controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.1;
        controls.screenSpacePanning = true;
        controls.target.set(0, 0.5, 0); // Adjust target based on expected model center

        // Add the model
        previewModel = modelScene.clone(); // Clone to avoid modifying the emitted scene

        // --- Basic Centering and Scaling for Preview ---
        const box = new THREE.Box3().setFromObject(previewModel);
        const size = box.getSize(new THREE.Vector3());
        const center = box.getCenter(new THREE.Vector3());

        const maxDim = Math.max(size.x, size.y, size.z);
        const scale = 3.0 / maxDim; // Adjust '3.0' for desired preview size
        previewModel.scale.setScalar(scale);

        // Recalculate box and center after scaling
        box.setFromObject(previewModel);
        box.getCenter(center);

        // Position model
        previewModel.position.x -= center.x;
        previewModel.position.y -= center.y; // Center vertically
        // previewModel.position.y -= box.min.y; // Alternative: Place bottom at y=0
        previewModel.position.z -= center.z;

        previewScene.add(previewModel);
        controls.target.copy(center.multiplyScalar(scale)); // Update controls target to scaled center

        controls.update(); // Apply target change

        // Start animation loop
        animatePreview();
        console.log('Preview setup complete.');

    } catch (previewError) {
        console.error("Error setting up preview:", previewError);
        errorMessage.value = "Failed to create preview.";
        cleanupPreview();
    }
}

function animatePreview() {
    if (!renderer) return; // Stop if cleaned up
    animationFrameId = requestAnimationFrame(animatePreview);
    controls?.update();
    renderer.render(previewScene, camera);
}

function cleanupPreview() {
    if (animationFrameId) {
        cancelAnimationFrame(animationFrameId);
        animationFrameId = null;
    }
    if (controls) {
        controls.dispose();
        controls = null;
    }
    if (renderer) {
        renderer.dispose(); // Dispose renderer resources
        // Remove canvas if needed, or just clear it
        const canvas = renderer.domElement;
        if (canvas) {
            const context = canvas.getContext('webgl2') || canvas.getContext('webgl');
            if (context) {
                // Attempt to clear context resources - effectiveness varies
                if (context.getExtension('WEBGL_lose_context')) {
                    context.getExtension('WEBGL_lose_context').loseContext();
                }
            }
            // Optionally remove canvas from DOM if dynamically added,
            // but here it's part of the template, so just let it be.
        }
        renderer = null;
    }
    // Dispose geometries and materials of the preview model clone
    if (previewModel) {
        previewModel.traverse(object => {
            if (object.isMesh) {
                object.geometry?.dispose();
                if (object.material) {
                    if (Array.isArray(object.material)) {
                        object.material.forEach(mat => mat.dispose());
                    } else {
                        object.material.dispose();
                    }
                }
            }
        });
        previewScene?.remove(previewModel); // Remove from scene
    }

    previewScene = null;
    camera = null;
    previewModel = null;
    console.log("Preview cleaned up.");
}

function cleanupObjectUrls() {
    if (objectURLs.value.length > 0) {
        console.log(`Revoking ${objectURLs.value.length} previous Blob URLs.`);
        objectURLs.value.forEach(url => URL.revokeObjectURL(url));
        objectURLs.value = []; // Clear the array
    }
}

// --- Lifecycle Hooks ---
onMounted(() => {
    // Optional: setup intersection observer for preview resize if needed
});

onUnmounted(() => {
    cleanupPreview(); // Clean up Three.js resources
    cleanupObjectUrls(); // Clean up any remaining blob URLs
});

// Watch for prop changes if necessary (e.g., if gltfPathInZip could change)
// watch(() => props.gltfPathInZip, (newPath, oldPath) => {
//    if (newPath !== oldPath) {
//       // Handle path change, maybe clear current state?
//       console.log("GLTF path prop changed:", newPath);
//       // Reset component state if needed
//       errorMessage.value = null;
//       loadedModelName.value = null;
//       cleanupPreview();
//       cleanupObjectUrls();
//    }
// });

</script>

<style scoped>
.gltf-zip-loader {
    border: 1px solid #ccc;
    border-radius: 8px;
    padding: 15px;
    display: flex;
    flex-direction: column;
    gap: 15px;
    /* Space between dropzone and preview */
    background-color: #f9f9f9;
}

.drop-zone {
    border: 2px dashed #ccc;
    border-radius: 8px;
    padding: 30px;
    text-align: center;
    cursor: pointer;
    transition: background-color 0.2s ease, border-color 0.2s ease;
    position: relative;
    /* For loading overlay */
    min-height: 150px;
    /* Ensure dropzone has some height */
    display: flex;
    /* Center content vertically */
    flex-direction: column;
    justify-content: center;
    align-items: center;
    color: #666;
}

.drop-zone.drag-over {
    background-color: #e0e0ff;
    border-color: #007bff;
}

.drop-zone.is-loading {
    cursor: not-allowed;
    background-color: #f0f0f0;
}

.drop-zone:focus {
    outline: 2px solid #007bff;
    outline-offset: 2px;
}


.placeholder-text {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    pointer-events: none;
    /* Allow clicks to pass through to drop-zone */
}

.placeholder-text svg {
    margin-bottom: 10px;
    color: #999;
}

.placeholder-text strong {
    color: #007bff;
}

.loading-overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(255, 255, 255, 0.8);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    border-radius: 8px;
    /* Match parent */
    z-index: 10;
    gap: 10px;
    color: #333;
    font-weight: bold;
}

.spinner {
    border: 4px solid #f3f3f3;
    /* Light grey */
    border-top: 4px solid #3498db;
    /* Blue */
    border-radius: 50%;
    width: 30px;
    height: 30px;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}

.status-message {
    font-size: 0.9em;
    pointer-events: none;
    /* Allow clicks to pass through */
}

.error-message {
    color: #dc3545;
    /* Bootstrap danger color */
    font-weight: bold;
}

.error-message p:first-child {
    margin-bottom: 5px;
}

.success-message {
    color: #28a745;
    /* Bootstrap success color */
    word-break: break-all;
    /* Prevent long filenames overflowing */
    padding: 0 10px;
    /* Add padding if text gets too wide */
}

.preview-area {
    border: 1px solid #ddd;
    border-radius: 4px;
    background-color: #fff;
    /* White background for preview */
    position: relative;
    /* For placeholder */
    min-height: 100px;
    /* Ensure it has height even without canvas */
}

.preview-area canvas {
    display: block;
    /* Remove extra space below canvas */
    width: 100%;
    /* Default width */
    height: auto;
    /* Default height */
    border-radius: 4px;
    /* Match container */
}

.preview-placeholder {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: #aaa;
    font-style: italic;
    font-size: 0.9em;
    margin: 0;
    pointer-events: none;
}
</style>